<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<canvas id="canvase01" width="400" height="300" style="border: solid 1px black;"></canvas>
		<br>
		X: <input id="el-x" type="number" value="50"/>  Y:<input id="el-y" type="number" value="50"/>
		<br>
		width: <input id="el-width" type="number" value="50"/>  height:<input id="el-height" type="number" value="20"/>
		<br>
		
		
		<input type="button" value="Rect"/>
		<br>
		
		<input type="button" value="ADD"/>
		
		<script>
			
			// 绘制圆角矩形
			function roundRect(ctx, x, y, width, height, radius) {
			  if (typeof radius === "undefined") {
			    radius = 5;
			  }
			  if (typeof radius === "number") {
			    radius = {tl: radius, tr: radius, br: radius, bl: radius};
			  } else {
			    var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
			    for (var side in defaultRadius) {
			      radius[side] = radius[side] || defaultRadius[side];
			    }
			  }
			  ctx.beginPath();
			  ctx.moveTo(x + radius.tl, y);
			  ctx.lineTo(x + width - radius.tr, y);
			  ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
			  ctx.lineTo(x + width, y + height - radius.br);
			  ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
			  ctx.lineTo(x + radius.bl, y + height);
			  ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
			  ctx.lineTo(x, y + radius.tl);
			  ctx.quadraticCurveTo(x, y, x + radius.tl, y);
			  ctx.closePath();
			}

			// 显示输入框
			function showInputBox(shape) {
			    const input = document.createElement("textarea");
			    // input.type = "text";
				
			    input.value = shape.text;
				// input.style.border = "none";           // 去掉边框
				// input.style.outline = "none";          // 去掉聚焦时的默认轮廓（蓝色/橙色高亮）
				// input.style.boxShadow = "none";        // 去掉可能的阴影（某些浏览器或主题下存在）
				// input.style.background = "transparent"; // 可选：透明背景，使其更像 Canvas 文本
				input.style.font="12pt Arial"
				input.style.textAlign="left"
				input.style.position = "absolute";
			    input.style.left = (shape.left + 20) + "px";
			    input.style.top = (shape.top + shape.height) + "px";
			    input.style.width = shape.width -20 + "px";
				input.style.height = shape.height +"px"
				
			    input.onblur = function() {
			        shape.text = input.value;
			        shape.isEditing = false;
					shape.setInvalidate()
			        input.remove();
			    };
					
				
			    document.body.appendChild(input);
			    input.focus();
			}

			// 全局变量
			let activeShape = null
			let isCaretVisible = true; // 光标是否可见
			let caretTimer = null;
			
			
			let dragStartX = 0;
			let dragStartY = 0;
			let isDragging = false;
			
			
			caretTimer = setInterval(() => {
				isCaretVisible = !isCaretVisible;
				}, 500);
			
			
			const LINE_HEIGHT_1 = 20
			const LINE_HEIGHT_2 = 18
			const LINE_HEIGHT_3 = 14
			const PADDING = 10
			const MARGIN = 10
			const OUTERGAP = PADDING + MARGIN
			
			const CURVE_XDIST = 100
			
			
			var IDGEN = 0;
			function genID(){
				IDGEN +=1
				return "g"+IDGEN;
			}
			
			class Canvase {
				constructor(id) {
					var cavs = document.getElementById(id)
					this.ctx = cavs.getContext('2d')
					this.canvas = cavs;
					this.shapeList= [];					// 实际再画的区域
					
					this.invalidateShapeList = [];		// shape调用 invalidate 将区域记录到这个位置, 再属性改变后，可以在重绘
					
					
					this.canvas.addEventListener('dblclick', (event)=>{ this.handleDbClick(event)})
					this.canvas.addEventListener('mousedown', (event)=>{this.handleMousedown(event)})
					this.canvas.addEventListener('mouseup', (event)=>{this.handleMouseup(event)})
					this.canvas.addEventListener('mousemove',(event)=>{this.handleMouseover(event)})
					
				}
				append(shap){
					this.shapeList.unshift(shap)
				}
				
				drawAll(){
					
					this.clearInvalide();
					// 重绘失效区域
					
					// this.shapeList.forEach((shap)=>{
					// 	if(shap.getInvalidate()){
					// 		shap.draw(this.ctx)
					// 		shap.invalidate = false;
					// 	}
					// })
					
					for(let i=this.shapeList.length-1; i>=0; i--){
						var shap = this.shapeList[i]
						if(shap.getInvalidate()){
							shap.draw(this.ctx)
							shap.invalidate = false;
						}
					}
				}
				
				// 计算重叠
				calculateOverlap(spitem){
					if(spitem.getInvalidate()){
						this.shapeList.forEach((shp)=>{
								if(!shp.getInvalidate()){
									if(spitem.isOverlap(shp) || shp.isOverlap(spitem)){
										shp.setInvalidate();
										this.calculateOverlap(shp);
									}
								}
						})
					}
				}
				
				// 清空重叠区域
				clearInvalide(){
					
					
					this.invalidateShapeList.forEach((invalid)=>{
						this.calculateOverlap(invalid)
					})
					
					this.shapeList.forEach((shap)=>{
						this.calculateOverlap(shap)
					})
					
					this.invalidateShapeList.forEach((invalid)=>{
						this.ctx.clearRect(invalid.left, invalid.top, invalid.width, invalid.height)
					})
					
					this.invalidateShapeList = []
					
					this.shapeList.forEach((shap)=>{
						if(shap.getInvalidate()){
							this.ctx.clearRect(shap.left, shap.top, shap.width, shap.height)
						}
					})
				}
			
			
				activeShape(shape, isEdit=false){					// 选中激活一个shape, 并设置是否编辑
					if(activeShape!=null){
						activeShape.isActive = false
						activeShape.isEditing = false;
					}
					
					activeShape = shape
					activeShape.isActive = true
					activeShape.isEditing = isEdit;
					
				}
			
				// 双击编辑shape的text文本
				handleDbClick(event){
					const rect = this.canvas.getBoundingClientRect();
					const x = event.clientX - rect.left;
					const y = event.clientY - rect.top;
					
					for(let s of this.shapeList){
						if(s.isPointIn(x, y)){
							this.activeShape(s, true)
							showInputBox(activeShape)
							break;
						}
					}
				}
			
				handleMousedown(event){
					const rect = this.canvas.getBoundingClientRect();
					const x = event.clientX - rect.left;
					const y = event.clientY - rect.top;
					for(let s of this.shapeList){
						if(s.isPointIn(x, y)){
							this.activeShape(s)
							dragStartX = x 
							dragStartY = y
							isDragging = true
							break;
						}
					}
				}
				
				handleMouseup(event){
					isDragging = false
				}
				
				handleMouseover(event){
					
					const rect = this.canvas.getBoundingClientRect();
					const x = event.clientX - rect.left;
					const y = event.clientY - rect.top;
					
					if(isDragging && activeShape.isPointIn(x, y)){
						 let dx = x - dragStartX
						 let dy = y - dragStartY
						 
						 activeShape.setInvalidate(this)
						 activeShape.left +=dx
						 activeShape.top +=dy
						 dragStartX = x 
						 dragStartY = y
					}else{
						isDragging = false
					}
				}
			}
			
			
			
			class Shape{
				constructor(x, y) {
				    this.left = x;
					this.top = y;
					
					this.width = 120;
					this.height = 40;
					this.radius = 0;
					
					this.text = "输入文字";
					
													// 处理过程中要使用的
					this.textSplits=null;			// 保存分割后的文本
					this.invalidate = true;
					
					this.gid = genID()
					
					this.isEditing = false;				//编辑状态
					this.draggable = true;				
					this.isActive = false;				// 选中状态
				}
				
				
				setInvalidate(ctxcavs = null){
					this.invalidate = true;
					if(ctxcavs !=null){
						let clone = this.cloneDrawArea()
						ctxcavs.invalidateShapeList.push(clone)
					}
				}
				
				
				getInvalidate(){
					return this.invalidate;
				}
				
				
				// 根据文字内容来调整区域的宽高
				autoJustWHByText(ctx){
					this.text = this.text.trimEnd()
					var strs = this.text.split('\n');
					this.textSplits = strs
					var linNum = strs.length
					
					this.height = 2*OUTERGAP + LINE_HEIGHT_1 * linNum - 8
					
					let max = 0;
					let metrics = 0;
					for(let st of strs){
						metrics = ctx.measureText(st);
						if(metrics.width > max)
							max = metrics.width
					}
					
					if(max>0){
						this.width = max > 60 ? max + 2*OUTERGAP : 100
					}else{
						this.width = 100
					}
					
				}
				
				draw(ctx){
					
					ctx.font = "12pt Arial"
					this.autoJustWHByText(ctx)
					
					roundRect(ctx, this.left + MARGIN, this.top + MARGIN, this.width - 2*MARGIN, this.height - 2*MARGIN,  16);
					ctx.lineWidth=5
					ctx.strokeStyle="#FF9900";
					ctx.stroke()
					
					ctx.fillStyle="#FFFFFF"
					ctx.fill()
					
					ctx.fillStyle="#000000"
					ctx.textAlign="left"
					ctx.textBaseline = "top"
					// ctx.fillText(this.text, this.left + OUTERGAP, this.top+OUTERGAP+LINE_HEIGHT_1)
					let ln = 0
					for(let str of this.textSplits){
						ctx.fillText(str, this.left + OUTERGAP, this.top + OUTERGAP+LINE_HEIGHT_1 * ln)
						ln ++;
					}
				}
				
				isOverlap(itemsp){
					var cx = itemsp.left
					var cy = itemsp.top
					if(this.isPointIn(cx, cy)) 
						return true
						
					cx = itemsp.left + itemsp.width
					if(this.isPointIn(cx, cy))
						return true
					
					cy = itemsp.top + itemsp.height
					if(this.isPointIn(cx, cy))
						return true
						
					cx = itemsp.left
					if(this.isPointIn(cx, cy))
						return true
						
				}
				
				isPointIn(x, y){
					if( x>=this.left && x<=(this.left+this.width)){
						if(y>=this.top && y<=(this.top + this.height)){
							return true;
						}
					}
					return false;
				}
				
				
				cloneDrawArea(){
					let clone = new Shape(this.left, this.top)
					clone.width = this.width
					clone.height = this.height
					clone.invalidate = true
					return clone
				}
			}
			
			// 平行的连接曲线
			class CurveLine extends Shape {
				constructor(start, end) {
					super(0, 0)
					
					this.start = start
					this.end = end
					
					if(start.x < end.x){
						this.left = start.x - MARGIN
					}else{
						this.left = end.x -MARGIN
					}
					
					if(start.y < end.y){
						this.top = start.y -MARGIN
					}else{
						this.top = end.y -MARGIN
					}
					this.width = Math.abs(start.x -end.x) + 2*MARGIN
					this.height = Math.abs(start.y-end.y) + 2*MARGIN
					
				}
				
				draw(ctx){
					let cp1 = {}
					let cp2 = {}
					
					cp1.y = this.start.y
					cp2.y = this.end.y
					if(this.start.x < this.end.x){
						cp1.x = this.start.x  + CURVE_XDIST
						cp2.x = this.end.x - CURVE_XDIST
					}else{
						cp1.x = this.start.x - CURVE_XDIST
						cp2.x = this.end.x + CURVE_XDIST
					}
					
					ctx.beginPath();
					ctx.moveTo(this.start.x, this.start.y);
					ctx.bezierCurveTo(
					  cp1.x, cp1.y,
					  cp2.x, cp2.y,
					  this.end.x, this.end.y
					);								
					ctx.lineWidth = 3;
					ctx.strokeStyle = 'blue';
					ctx.stroke();
				}
				
				
			}
			
			
			
			
			var MyCanvase = new Canvase("canvase01")
			var shap1 =new Shape(50, 50)
			var shap2 =new Shape(100, 100)
			
			
			var curshape1 = new CurveLine( {x:100, y:200}, {x:300, y:50})
			
			MyCanvase.append(shap1);
			MyCanvase.append(shap2)
			MyCanvase.append(curshape1)
			
			function ReDraw(){
				MyCanvase.drawAll();
				requestAnimationFrame(ReDraw)
			}
			requestAnimationFrame(ReDraw)
			
			
			const ctx = document.getElementById('canvase01').getContext('2d');
			
			  const startPoint = { x: 50, y: 250 };
			  const cp1 = { x: 150, y: 250 };
			  const cp2 = { x: 200, y: 50 };
			  const endPoint = { x: 350, y: 50 };
			
			  ctx.beginPath();
			  ctx.moveTo(startPoint.x, startPoint.y);
			  ctx.bezierCurveTo(
			    cp1.x, cp1.y,
			    cp2.x, cp2.y,
			    endPoint.x, endPoint.y
			  );
			
			  ctx.lineWidth = 3;
			  ctx.strokeStyle = 'green';
			  ctx.stroke();
			
			  // 绘制辅助线
			  ctx.setLineDash([5, 5]);
			  ctx.beginPath();
			  ctx.moveTo(startPoint.x, startPoint.y);
			  ctx.lineTo(cp1.x, cp1.y);
			  ctx.moveTo(cp2.x, cp2.y);
			  ctx.lineTo(endPoint.x, endPoint.y);
			  ctx.strokeStyle = 'gray';
			  ctx.stroke();
			
			  // 绘制控制点
			  ctx.setLineDash([]);
			  ctx.fillStyle = 'red';
			  ctx.beginPath();
			  ctx.arc(cp1.x, cp1.y, 5, 0, Math.PI * 2);
			  ctx.arc(cp2.x, cp2.y, 5, 0, Math.PI * 2);
			  ctx.fill();
			  
			
		</script>
	</body>
</html>